/* Copyright (c) 2009 Mark L. Howison
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  (1) Redistributions of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 *  (2) Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *  (3) The name of the copyright holder may not be used to endorse or promote
 *      products derived from this software without specific prior written
 *      permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package edu.berkeley.edrl.wiikinemathics;

import apple.dts.samplecode.osxadapter.OSXAdapter;
import java.awt.Color;
import java.awt.Dimension;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import org.jdesktop.application.Action;
import org.jdesktop.application.Application;
import org.jdesktop.application.SingleFrameApplication;
import wiiremotej.IRSensitivitySettings;
import wiiremotej.WiiRemote;
import wiiremotej.WiiRemoteJ;
import wiiremotej.event.WRIREvent;
import wiiremotej.event.WiiDeviceDiscoveredEvent;
import wiiremotej.event.WiiDeviceDiscoveryListener;

abstract class MotionUpdater {
    abstract void update(double t, double ratio);
}

/**
 * The main class of the application.
 */
public class WiiKinemathicsApp extends SingleFrameApplication
        implements WiiDeviceDiscoveryListener
{
    public final static boolean DEBUG = false;
    
    public final static int INPUT_WIIMOTE = 0;
    public final static int INPUT_GAMETRAK = 1;
    public final static int INPUT_DEVICE = INPUT_WIIMOTE;

    public final static boolean MAC_OS_X = System.getProperty("os.name").toLowerCase().startsWith("mac os x");

    public final static double WIICAM_WIDTH  = 1024.0;
    public final static double WIICAM_HEIGHT = 768.0;
    public final static double WIICAM_DOTSIZE = 16.0;
    public final static int WIICAM_SENSORS = 4;

    private final static long CLOCK_TICK_MS = 10;
    private final static long PAUSE_MS = 100;

    private boolean connected = false;
    private Thread displayThread;

    WiiRemote remote;
    ControlPanel controlPanel;
    MotionPanel motionPanel;
    CameraPanel cameraPanel;
    DriverPanel driverPanel;
    JFrame motionWindow;
    JFrame cameraWindow;
    JFrame driverWindow;
    MotionTracker motionTracker;
    MotionDriver motionDriver;
    WiiKinemathicsAboutBox aboutBox;
    WiiKinemathicsView view;
    MotionUpdater targeter;
    MotionUpdater driver;

    /**
     * At startup create and show the main frame of the application.
     */
    @Override protected void startup() {

        if (WiiKinemathicsApp.INPUT_DEVICE == WiiKinemathicsApp.INPUT_WIIMOTE) {
            System.setProperty("bluecove.jsr82.psm_minimum_off", "true");
        }

        controlPanel = new ControlPanel();
        motionPanel = new MotionPanel();
        cameraPanel = new CameraPanel();
        driverPanel = new DriverPanel();
        motionTracker = new MotionTracker();
        motionDriver = new MotionDriver();

        targeter = new MotionUpdater() {
            void update(double t, double ratio) {
                motionPanel.updateTarget(t*ratio, t, 1.0-t);
                motionPanel.repaint();
            }
        };

        driver = new MotionUpdater() {
            void update(double t, double ratio) {
                motionPanel.updateTarget(
                        motionDriver.getCorrected(t*ratio),
                        motionDriver.getCorrected(t),
                        1.0-t);
                motionPanel.updateDriver(
                        motionDriver.getCorrected(t*ratio),
                        motionDriver.getCumulative(t, ratio));
                motionPanel.repaint();
            }
        };

        if (WiiKinemathicsApp.INPUT_DEVICE == WiiKinemathicsApp.INPUT_WIIMOTE) {
            WiiRemoteJ.setConsoleLoggingErrors();
            Logger.getLogger("wiiremotej").setLevel(Level.ALL);
        }

        view = new WiiKinemathicsView(this);
        show(view);

        motionWindow = new JFrame("Motion");
        motionWindow.setPreferredSize(new Dimension(200,800));
        motionWindow.getContentPane().add(motionPanel);
        motionWindow.pack();
        motionWindow.setVisible(true);
        
        cameraWindow = new JFrame("Camera");
        cameraWindow.setPreferredSize(new Dimension(640,480));
        cameraWindow.getContentPane().add(cameraPanel);
        cameraWindow.pack();
        cameraWindow.setVisible(true);

        driverWindow = new JFrame("Driver");
        //driverWindow.setPreferredSize(new Dimension(300,300));
        driverWindow.getContentPane().add(driverPanel);
        driverWindow.pack();
        driverWindow.setVisible(true);

        aboutBox = new WiiKinemathicsAboutBox(view.getFrame());

        if (MAC_OS_X) {
            try {
                OSXAdapter.setQuitHandler(this, Application.class.getDeclaredMethod("exit", (Class[])null));
                OSXAdapter.setAboutHandler(aboutBox, WiiKinemathicsAboutBox.class.getDeclaredMethod("openAboutBox", (Class[])null));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (WiiKinemathicsApp.INPUT_DEVICE == WiiKinemathicsApp.INPUT_WIIMOTE) {
            connect();
        } else {
            getLogger().info("Polling socket to GameTrak Max patch");
            Thread trakThread = new Thread(new Runnable() {
                public void run() {
                    try {
                        InetAddress group = InetAddress.getByName("230.0.0.1");
                        MulticastSocket socket = new MulticastSocket(3001);
                        socket.joinGroup(group);
                        while (true) {
                            byte[] buf = new byte[32];
                            DatagramPacket recv = new DatagramPacket(buf, buf.length);
                            socket.receive(recv);
                            motionTracker.gameTrak(recv.getData());
                            Thread.sleep(CLOCK_TICK_MS);
                        }
                    } catch (IOException e) {
                        WiiKinemathicsApp.getLogger().severe("Could not open socket to GameTrak Max patch");
                    } catch (InterruptedException e) {
                    }
                }
            });
            trakThread.start();
        }
    }

    public void showMotionWindow() {
        motionWindow.setVisible(true);
        motionWindow.toFront();;
    }

    public void showCameraWindow() {
        cameraWindow.setVisible(true);
        cameraWindow.toFront();;
    }

    @Override
    protected void shutdown() {
        super.shutdown();
        getLogger().info("Disconnecting wiimote on shutdown.");
        if (remote != null) remote.disconnect();
    }

    @Action
    public void connect() {
        disconnect();
        controlPanel.connectLabel.setText("[Searching...]");
        controlPanel.connectLabel.setForeground(Color.ORANGE);
        try {
            WiiRemoteJ.findRemotes(WiiKinemathicsApp.getApplication());
        } catch (IllegalStateException e) {
            getLogger().warning("Illegal state for WiiRemoteJ.");
        }
    }

    public void disconnect() {
        WiiRemoteJ.stopFind();
        connected = false;
        controlPanel.connectLabel.setText("[Disconnected]");
        controlPanel.connectLabel.setForeground(Color.RED);
        if (remote != null) {
            if (remote.isConnected()) {
                remote.removeWiiRemoteListener(motionTracker);
                remote.disconnect();
            }
        }
    }

    public void updateBattery(WiiRemote remote, double level) {
        if (this.remote.equals(remote)) {
            
        }
    }

    /**
     * This method is to initialize the specified window by injecting resources.
     * Windows shown in our application come fully initialized from the GUI
     * builder, so this additional configuration is not needed.
     */
    @Override protected void configureWindow(java.awt.Window root) {
    }

    /**
     * A convenient static getter for the application instance.
     * @return the instance of WiiKinemathicsApp
     */
    public static WiiKinemathicsApp getApplication() {
        return Application.getInstance(WiiKinemathicsApp.class);
    }

    public static ControlPanel getControls() {
        return getApplication().controlPanel;
    }

    /**
     * Main method launching the application.
     */
    public static void main(String[] args) {
        if (MAC_OS_X) {
            System.setProperty("apple.laf.useScreenMenuBar", "true");
            System.setProperty("com.apple.mrj.application.apple.menu.about.name",
                    Application.getInstance().getContext().getResourceMap(WiiKinemathicsApp.class).getString("Application.title"));
        }
        System.setProperty("bluecove.jsr82.psm_minimum_off", "true");
        launch(WiiKinemathicsApp.class, args);
    }

    public void start() {
        controlPanel.enableControls(false);
        displayThread = new Thread(new Runnable() {
            public void run() {
                Object option = controlPanel.trialOption.getSelected();
                motionTracker.calibrate();
                motionPanel.resetHeight();
                if (!motionPanel.countDown("Calibrating",3)) return;
                if (motionTracker.isCalibrated()) {
                    if (option == ControlPanel.TRIAL_TIMED) {
                        if (!motionPanel.countDown("Tracking",3)) return;
                    }
                    motionTracker.track();
                    if (option == ControlPanel.TRIAL_TIMED) {
                        if (!runClock(targeter)) return;
                    }
                }
                else {
                    if (!motionPanel.abort()) return;
                }
            }
        });
        displayThread.start();
    }

    public void preview() {
        controlPanel.enableControls(false);
        displayThread = new Thread(new Runnable() {
            public void run() {
                if (!runClock(targeter)) return;
            }
        });
        displayThread.start();
    }

    public void drive() {
        controlPanel.enableControls(false);
        if (!motionDriver.validate()) {
            JOptionPane.showMessageDialog(driverWindow,
                    "Make sure your lists have the same length and contain only numbers.",
                    "Oops!",
                    JOptionPane.ERROR_MESSAGE);
        } else {
            displayThread = new Thread(new Runnable() {
                public void run() {
                    if (!runClock(driver)) return;
                }
            });
            displayThread.start();
        }
    }

    public void abort() {
        Logger.getLogger("WiiKinemathics").info("Aborting.");
        if (displayThread != null) {
            displayThread.interrupt();
        };
        motionPanel.reset();
        motionTracker.idle();
        controlPanel.enableControls(true);
    }

    private boolean runClock(MotionUpdater updater) {
        motionPanel.reset();
        double ratio = controlPanel.getRatio();
        double norm = 1.0 / Math.max(1.0, ratio);
        long ms_current, ms_start, ms_time;
        ms_time = (long)controlPanel.timeSlider.getValue();
        double step = 1.0 / ms_time;
        ms_current = 0;
        ms_start = System.currentTimeMillis();
        // upward motion
        while (ms_current < ms_time) {
            if (controlPanel.pauseCheckBox.isSelected()) {
                try {
                    Thread.sleep(PAUSE_MS);
                } catch (InterruptedException e) {
                    Logger.getLogger("WiiKinemathics").info("Interrupted.");
                    return false;
                }
                ms_start = System.currentTimeMillis() - ms_current;
            } else {
                ms_current = System.currentTimeMillis() - ms_start;
                double t = (double)ms_current * step * norm;
                updater.update(t, ratio);
                try {
                    Thread.sleep(CLOCK_TICK_MS);
                } catch (InterruptedException e) {
                    Logger.getLogger("WiiKinemathics").info("Interrupted.");
                    return false;
                }
            }
        }
        updater.update(1.0, ratio);
        // downward motion
        if (controlPanel.downwardOption.isSelected()) {
            ms_current = 0;
            ms_start = System.currentTimeMillis();
            while (ms_current < ms_time) {
                if (controlPanel.pauseCheckBox.isSelected()) {
                    try {
                        Thread.sleep(PAUSE_MS);
                    } catch (InterruptedException e) {
                        Logger.getLogger("WiiKinemathics").info("Interrupted.");
                        return false;
                    }
                    ms_start = System.currentTimeMillis() - ms_current;
                } else {
                    ms_current = System.currentTimeMillis() - ms_start;
                    double t = (double)((ms_time - ms_current) * step * norm);
                    updater.update(t, ratio);
                    try {
                        Thread.sleep(CLOCK_TICK_MS);
                    } catch (InterruptedException e) {
                        Logger.getLogger("WiiKinemathics").info("Interrupted.");
                        return false;
                    }
                }
            }
            updater.update(0.0, ratio);
        }
        motionTracker.idle();
        controlPanel.enableControls(true);
        return true;
    }

    static public Logger getLogger() {
        return Logger.getLogger("WiiKinemathics");
    }

    public void wiiDeviceDiscovered(WiiDeviceDiscoveredEvent e) {
        if (e.getWiiDevice() instanceof WiiRemote) {
            if (!connected) {
                remote = (WiiRemote)e.getWiiDevice();
                if (remote.isConnected()) {
                    controlPanel.connectLabel.setText("[Connecting...]");
                    controlPanel.connectLabel.setForeground(Color.CYAN);
                    WiiRemoteJ.stopFind();
                    getLogger().info("Found remote at address: " + remote.getBluetoothAddress());
                    connected = true;
                    controlPanel.connectLabel.setText("[Connected]");
                    controlPanel.connectLabel.setForeground(Color.GREEN);
                    remote.addWiiRemoteListener(motionTracker);
                    try {
                        remote.setLEDIlluminated(0, true);
                    } catch (IOException ex) {
                        getLogger().warning("Can't set LED illumination.");
                    }
                    try {
                        remote.setIRSensorEnabled(true,
                                WRIREvent.EXTENDED,
                                IRSensitivitySettings.WII_LEVEL_2);
                    } catch (Exception ex) {
                        getLogger().warning("Can't enable IR sensor.");
                    }
                }
            }
        }
    }

    public void findFinished(int numberFound) {
        getLogger().info("Found " + String.valueOf(numberFound) + " remote.");
    }
}
